﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq.Expressions;
using System.Reflection;
using System.Windows.Forms;

namespace SyringeControl.WinForms
{
    /// <summary>
    /// A set of extension methods simplifying data binding and subscribing on <see cref="INotifyPropertyChanged"/>
    /// Uses <see cref="System.Linq.Expressions.Expression{TDelegate}"/> to avoid passing strings as property names.
    /// </summary>
    public static class BindingExtensions
    {
        /// <summary>
        /// Subscribes on <see cref="INotifyPropertyChanged.PropertyChanged"/> notifications about <paramref name="modelProperty"/> changes.
        /// When such a notification is received, <paramref name="action"/> is executed with the new value of property.
        /// </summary>
        /// <typeparam name="TModel">type of object which properties need to be observed</typeparam>
        /// <typeparam name="TField">type of observed field</typeparam>
        /// <param name="model">instance of <typeparamref name="TModel"/> which will be observed</param>
        /// <param name="modelProperty">expression describing observed property</param>
        /// <param name="action">action executed when <paramref name="modelProperty"/> is changed</param>
        public static void OnChange<TModel, TField>(this TModel model, Expression<Func<TModel, TField>> modelProperty, Action<TField> action)
            where TModel: INotifyPropertyChanged
        {
            #region Argument validation
            if (model == null) throw new ArgumentNullException("model");
            if (modelProperty == null) throw new ArgumentNullException("modelProperty");
            if(action == null) throw new ArgumentNullException("action");
            var propExpr = modelProperty.Body as MemberExpression;
            if(propExpr == null || propExpr.Member.MemberType != MemberTypes.Property)
                throw new ArgumentException("Expression should represent access to property", "modelProperty");
            #endregion
            var prop = propExpr.Member as PropertyInfo;
            var getValue = modelProperty.Compile();
            model.PropertyChanged += (sender, e) => {
                        if (e.PropertyName == prop.Name)
                            action(getValue(model));
                    };
        }

        /// <summary>
        /// Binds model property to a control property.
        /// </summary>
        /// <typeparam name="TModel">type of object which properties need to be observed</typeparam>
        /// <typeparam name="TControl">type of visual control which is bound to model property</typeparam>
        /// <typeparam name="TModelField">type of observed property</typeparam>
        /// <typeparam name="TControlField">type of control property which is bound to <typeparamref name="TModelField"/></typeparam>
        /// <param name="model">instance of <typeparamref name="TModel"/> which will be observed</param>
        /// <param name="control">visual control which property will be bound to model</param>
        /// <param name="modelProperty">expression describing property of model being observed</param>
        /// <param name="controlProperty">expression describing property of <paramref name="control"/> being bound to <paramref name="modelProperty"/></param>
        public static void Bind<TModel, TControl, TModelField, TControlField>(this TModel model, TControl control,
            Expression<Func<TModel, TModelField>> modelProperty, Expression<Func<TControl, TControlField>> controlProperty)
            where TModel: INotifyPropertyChanged
            where TControl: Control
        {
            #region ArgumentValidation
            if (model == null) throw new ArgumentNullException("model");
            if (modelProperty == null) throw new ArgumentNullException("modelProp");
            if (control == null) throw new ArgumentNullException("control");
            if (controlProperty == null) throw new ArgumentNullException("controlProp");
            var modelPropExpr = modelProperty.Body as MemberExpression;
            if (modelPropExpr == null || modelPropExpr.Member.MemberType != MemberTypes.Property)
                throw new ArgumentException("Expression should represent access to property", "modelProperty");
            var controlPropExpr = controlProperty.Body as MemberExpression;
            if (controlPropExpr == null || controlPropExpr.Member.MemberType != MemberTypes.Property)
                throw new ArgumentException("Expression should represent access to property", "controlPropExpr");
            #endregion
            var binding = new SimpleBindingSettings<TModel, TControl, TModelField, TControlField>(model, control, modelProperty, controlProperty);
        }
        /// <summary>
        /// Binds control representing a list of objects to a list property of <typeparamref name="TModel"/>
        /// </summary>
        /// <typeparam name="TModel">type of object which properties need to be observed</typeparam>
        /// <typeparam name="TItem">type of item observed list</typeparam>
        /// <param name="model">instance of <typeparamref name="TModel"/> which will be observed</param>
        /// <param name="control">visual control which property will be bound to model</param>
        /// <param name="listProperty">expression describing a list property of model being observed</param>
        public static ListBindingSettings<TModel, TItem> BindList<TModel, TItem>(this TModel model, ListControl control,
            Expression<Func<TModel, IList<TItem>>> listProperty)
        {
            return new ListBindingSettings<TModel, TItem>(model, control, listProperty);
        }

                class SimpleBindingSettings<TModel, TControl, TModelField, TControlField> where TControl: Control
        {
            readonly Binding _binding;
            readonly TControl _control;
            public SimpleBindingSettings(TModel model, TControl control, 
                Expression<Func<TModel, TModelField>> modelPropertyExpr,
                Expression<Func<TControl, TControlField>> controlPropertyExpr)
            {
                _control = control;
                var modelProp = (modelPropertyExpr.Body as MemberExpression).Member as PropertyInfo;
                var ctrlProp = (controlPropertyExpr.Body as MemberExpression).Member as PropertyInfo;
                _binding = new Binding(ctrlProp.Name, model, modelProp.Name, true, DataSourceUpdateMode.OnPropertyChanged) 
                { 
                    ControlUpdateMode = ControlUpdateMode.OnPropertyChanged,
                };
                control.DataBindings.Add(_binding);
            }

            public SimpleBindingSettings<TModel, TControl, TModelField, TControlField> Display<TItemField>(
                Expression<Func<TModelField, TItemField>> itemField)
            {
                throw new NotImplementedException();
            }
        }

        /// <summary>
        /// A binding between property of <typeparamref name="TModel"/> implementing <see cref="System.Collections.Generic.IList{TItem}"/> and <see cref="System.Windows.Forms.ListControl"/>.
        /// </summary>
        /// <typeparam name="TModel">type of model containing property</typeparam>
        /// <typeparam name="TItem">type of item of bound model property</typeparam>
        public class ListBindingSettings<TModel, TItem>
        {
            readonly BindingSource _bindingSource;
            readonly ListControl _control;
            /// <summary>
            /// Initializes an instance of <see cref="ListBindingSettings{TModel,TItem}"/>.
            /// </summary>
            /// <param name="model">object which property should be bound to <see cref="System.Windows.Forms.ListControl"/>.</param>
            /// <param name="control">a control that will visualize the property</param>
            /// <param name="modelProperty">an expression describing property</param>
            public ListBindingSettings(TModel model, ListControl control, Expression<Func<TModel, IList<TItem>>> modelProperty)
            {
                if (model == null) throw new ArgumentNullException("mode");
                if (control == null) throw new ArgumentNullException("control");
                if (modelProperty == null) throw new ArgumentNullException("propertyExpr");
                var propExpr = modelProperty.Body as MemberExpression;
                if (propExpr == null || propExpr.Member.MemberType != MemberTypes.Property)
                    throw new ArgumentException("Can only bind to properties", "modelProperty");
                var member = propExpr.Member;
                _control = control;
                _bindingSource = new BindingSource(model, member.Name);
                _control.DataSource = _bindingSource;
            }

            /// <summary>
            /// Makes <see cref="ListBindingSettings{TModel,TItem}"/> to display a given field for each item.
            /// </summary>
            /// <typeparam name="TItemField">type of field that needs to be displayed</typeparam>
            /// <param name="itemField">an expression describing field being displayed</param>
            /// <returns>modified <see cref="ListBindingSettings{TModel,TItem}"/></returns>
            public ListBindingSettings<TModel, TItem> Display<TItemField>(Expression<Func<TItem, TItemField>> itemField)
            {
                if (itemField == null) throw new ArgumentNullException("itemField");
                var memberExpr = itemField.Body as MemberExpression;
                if (memberExpr == null || memberExpr.Member.MemberType != MemberTypes.Property)
                    throw new ArgumentException("Can display only properties", "itemField");
                var member = (itemField.Body as MemberExpression).Member;
                _control.DisplayMember = member.Name;
                return this;
            }
            
            /// <summary>
            /// Makes <see cref="ListBindingSettings{TModel,TItem}"/> to change given model property when currently selected value of list is changed.
            /// </summary>
            /// <param name="currentValueProperty">an expression describing property that shall receive new selected value</param>
            /// <returns>modified <see cref="ListBindingSettings{TModel,TItem}"/></returns>
            public ListBindingSettings<TModel, TItem> CurrentValue(Expression<Func<TModel, TItem>> currentValueProperty)
            {
                if (currentValueProperty == null)
                    throw new ArgumentNullException("currentValueProperty");
                var memberExpr = currentValueProperty.Body as MemberExpression;
                if (memberExpr == null || memberExpr.Member.MemberType != MemberTypes.Property || ! typeof(TModel).IsAssignableFrom(memberExpr.Member.DeclaringType))
                    throw new ArgumentException("Can bind current value only to properties");
                var member = memberExpr.Member as PropertyInfo;
                _bindingSource.CurrentItemChanged += delegate
                {
                    member.SetValue(_bindingSource.DataSource, _bindingSource.Current, null);
                };
                return this;
            }
        }
    }
}
